{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### cuIndicator Performance test\n",
    "cuIndicator is implemented by [cuDF](https://github.com/rapidsai/cudf) and [Numba](http://numba.pydata.org/) libraries. All the indicators are accelerated in the GPU via simple python code. \n",
    "\n",
    "In this notebook, we are going to demo 1) the performance gains and 2) how simple to implement customized indicators.\n",
    "\n",
    "We are going to use the common [Relative strength index](https://en.wikipedia.org/wiki/Relative_strength_index) indicator as an example in this notebook. We first load all the necessary libraries "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "sys.path.append('../..')\n",
    "import pandas as pd\n",
    "import cudf\n",
    "from gquant.cuindicator import Ewm, Rolling, shift, division, multiply, summation\n",
    "import numpy as np\n",
    "from bqplot.traits import convert_to_date\n",
    "import bqplot.pyplot as plt\n",
    "import ipywidgets as widgets\n",
    "from IPython.display import display\n",
    "import datetime\n",
    "import math\n",
    "from bqplot import OHLC, LinearScale, DateScale, Axis, Figure, Bars, CATEGORY10, OrdinalScale, Lines, Tooltip\n",
    "from bqplot.colorschemes import CATEGORY20\n",
    "import time\n",
    "from numba import cuda\n",
    "from numba import njit\n",
    "from numba import prange\n",
    "import math\n",
    "from dateutil import relativedelta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We generated fake stock 5min bar data for 20-30 years randomly and load them into the cuDF dataframe. We also define some constants for the plotting"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "@njit\n",
    "def compute_prices(open_p, close_p, low_p, high_p, norms, s0,\n",
    "                  sigma, mu):\n",
    "    length = len(open_p)\n",
    "    barsize = len(norms) // length\n",
    "    price = s0\n",
    "    for i in range(length):\n",
    "        first = False\n",
    "        low_p[i] = np.inf\n",
    "        high_p[i] = -np.inf\n",
    "        for bar_id in range(barsize):\n",
    "            price = price + price * mu + price * sigma * norms[i * barsize + bar_id]\n",
    "            if not first:\n",
    "                open_p[i] = price\n",
    "                first = True\n",
    "            if price < low_p[i]:\n",
    "                low_p[i] = price\n",
    "            if price > high_p[i]:\n",
    "                high_p[i] = price\n",
    "        close_p[i] = price\n",
    "\n",
    "\n",
    "\n",
    "def df_onestock(stock_id=0,\n",
    "                S0=20,\n",
    "                yearly_return=0.1,\n",
    "                yearly_sigma=0.1,\n",
    "                year_beg=1991,\n",
    "                year_end=1999,\n",
    "                volume_lam=50,\n",
    "                snapshots=10,\n",
    "                barfreq=5):\n",
    "    num_years = year_end - year_beg\n",
    "    one_day = relativedelta.relativedelta(days=1)\n",
    "    beg_time = \" 09:00\"\n",
    "    end_time = \" 16:00\"\n",
    "    start = pd.Timestamp('%s-1-1' % (year_beg))\n",
    "    end = pd.Timestamp('%s-1-1' % (year_end))\n",
    "\n",
    "    all_times = []\n",
    "    day = start\n",
    "    while day < end:\n",
    "        if day.weekday() <= 5:\n",
    "            day_str = day.strftime('%Y-%m-%d')\n",
    "            times = pd.date_range(day_str + beg_time,\n",
    "                                  day_str + end_time,\n",
    "                                  freq=(\"%dmin\"%(barfreq)))\n",
    "            all_times.extend(times)\n",
    "        day += one_day\n",
    "\n",
    "    mu =  yearly_return * num_years\n",
    "    sigma = np.sqrt(yearly_sigma**2.0 * num_years)\n",
    "\n",
    "\n",
    "    steps = len(all_times)\n",
    "\n",
    "    step_mu = mu / (snapshots * steps)\n",
    "    step_sigma = np.sqrt(sigma**2 / (snapshots * steps))\n",
    "\n",
    "    open_p = np.zeros(steps)\n",
    "    close_p = np.zeros(steps)\n",
    "    high_p = np.zeros(steps)\n",
    "    low_p = np.zeros(steps)\n",
    "    norms = np.random.normal(0, 1, steps * snapshots)\n",
    "    compute_prices(open_p, close_p, low_p, high_p, norms, S0, step_sigma, step_mu)\n",
    "    volume = np.random.poisson(volume_lam, steps).astype(np.float64)\n",
    "\n",
    "    df = pd.DataFrame({'Open':open_p, 'Close':close_p, 'High':high_p, 'Low':low_p,\n",
    "                       'Volume':volume, 'Dte':all_times})\n",
    "    df['Sm_id'] = stock_id\n",
    "    return df\n",
    "\n",
    "def gendata(num_stocks = 10, barfreq=5):\n",
    "    stocks = []\n",
    "    for sid in range(num_stocks):\n",
    "        print('gen stock %d/%d' %(sid, num_stocks))\n",
    "        s0 = np.random.normal(30.0, 5.0)\n",
    "        ret = np.random.normal(0.1, 0.2)\n",
    "        sig = np.random.normal(0.1, 0.2)\n",
    "        year_beg = np.random.randint(1991, 2001)\n",
    "        year_end = np.random.randint(2005, 2015)\n",
    "        volume_lam = np.random.randint(20, 80)\n",
    "        stock = df_onestock(stock_id=sid,\n",
    "                             S0=s0,\n",
    "                             yearly_return=ret,\n",
    "                             yearly_sigma=sig,\n",
    "                             year_beg=year_beg,\n",
    "                             year_end=year_end,\n",
    "                             volume_lam=volume_lam,\n",
    "                             barfreq=barfreq)\n",
    "        stocks.append(stock)\n",
    "    return pd.concat(stocks)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "gen stock 0/2\n",
      "gen stock 1/2\n"
     ]
    }
   ],
   "source": [
    "all_stock = gendata(2, 1) \n",
    "df = cudf.DataFrame.from_pandas(all_stock)\n",
    "df['Dte'] = df['Dte'].astype('datetime64[ms]')\n",
    "ids = all_stock.Sm_id.unique()\n",
    "list_stocks = { 'stock%d' % (i) : i for i in ids}\n",
    "\n",
    "indicator_lists = ['RSI(GPU)', 'RSI(CPU)']\n",
    "main_figure_height='300px'\n",
    "indicator_figure_height='150px'\n",
    "figure_width = '1500px'\n",
    "plot_dp = 300"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once we have all the data loaded in the GPU framebuffer, we can use cuDF dataframe to slice dice it. Here we define two utility functions to select different stock and year of the data. The query happnes in the GPU so it is very fast. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def one_stock(df, stock_id):\n",
    "    return df.query('Sm_id==%s' % stock_id)\n",
    "\n",
    "def slice_stock(df, year):\n",
    "    beg_date = datetime.datetime.strptime(str(year)+'-01-01', '%Y-%m-%d')\n",
    "    end_date = datetime.datetime.strptime(str(int(year)+1)+'-01-01', '%Y-%m-%d')\n",
    "    return df.query('Dte<@end_date and Dte>=@beg_date')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here we copy paste the RSI computation code from this open source project [Pandas Technical Indicators](git@github.com:Crypto-toolbox/pandas-technical-indicators.git). And we defined a wrapper function `cpu_rsi` to take the same input as the GPU version. To make a fair comparison with GPU, the RSI computation is accelerated by `numba.njit`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "@njit(fastmath=True, parallel=True)\n",
    "def updown_movement(high, low, UpI, DoI):\n",
    "    UpI[0] = 0\n",
    "    DoI[0] = 0\n",
    "    for i in prange(len(high)- 1):\n",
    "        UpMove = high[i + 1] - high[i]\n",
    "        DoMove = low[i] - low[i + 1]\n",
    "        if UpMove > DoMove and UpMove > 0:\n",
    "            UpI[i + 1] = UpMove\n",
    "        else:\n",
    "            UpI[i + 1] = 0\n",
    "        if DoMove > UpMove and DoMove > 0:\n",
    "            DoI[i + 1] = DoMove\n",
    "        else:\n",
    "            DoI[i + 1] = 0\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "def relative_strength_index(df, n):\n",
    "    \"\"\"Calculate Relative Strength Index(RSI) for given data.\n",
    "\n",
    "    :param df: pandas.DataFrame\n",
    "    :param n:\n",
    "    :return: pandas.DataFrame\n",
    "    \"\"\"\n",
    "    UpI = np.zeros(len(df))\n",
    "    DoI = np.zeros(len(df))\n",
    "    updown_movement(df['High'].values, df['Low'].values, UpI, DoI)\n",
    "    UpI = pd.Series(UpI)\n",
    "    DoI = pd.Series(DoI)\n",
    "    PosDI = pd.Series(UpI.ewm(span=n, min_periods=n).mean())\n",
    "    NegDI = pd.Series(DoI.ewm(span=n, min_periods=n).mean())\n",
    "    RSI = pd.Series(PosDI / (PosDI + NegDI), name='RSI')\n",
    "    df = df.join(RSI)\n",
    "    return df\n",
    "\n",
    "def cpu_rsi(high, low, n):\n",
    "    df = pd.DataFrame()\n",
    "    df['High'] = high.to_array()\n",
    "    df['Low'] = low.to_array()\n",
    "    out = relative_strength_index(df, n)\n",
    "    return out['RSI'].as_matrix()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Equivalently this is cuIndicator version of the RSI computation, we copy the code from the cuIndicator library. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "number_of_threads = 128\n",
    "\n",
    "@cuda.jit\n",
    "def up_down_kernel(high_arr, low_arr, upD_arr, doD_arr, arr_len):\n",
    "    i = cuda.grid(1)\n",
    "    if i < arr_len - 1:\n",
    "        if (math.isnan(high_arr[i]) or math.isnan(high_arr[i + 1])\n",
    "            or math.isnan(low_arr[i]) or math.isnan(low_arr[i + 1])):\n",
    "            upD_arr[i] = math.nan\n",
    "            doD_arr[i] = math.nan\n",
    "        else:\n",
    "            upMove = high_arr[i + 1] - high_arr[i]\n",
    "            doMove = low_arr[i] - low_arr[i + 1]\n",
    "            if upMove > doMove and upMove > 0:\n",
    "                upD_arr[i] = upMove\n",
    "            else:\n",
    "                upD_arr[i] = 0\n",
    "            if doMove > upMove and doMove > 0:\n",
    "                doD_arr[i] = doMove\n",
    "            else:\n",
    "                doD_arr[i] = 0\n",
    "    elif i == arr_len - 1:\n",
    "        upD_arr[i] = math.nan\n",
    "        doD_arr[i] = math.nan\n",
    "        \n",
    "def upDownMove(high_arr, low_arr):\n",
    "    upD_arr = cuda.device_array_like(high_arr)\n",
    "    doD_arr = cuda.device_array_like(high_arr)\n",
    "    array_len = len(high_arr)\n",
    "    number_of_blocks = (array_len + (\n",
    "        number_of_threads - 1)) // number_of_threads\n",
    "    up_down_kernel[(number_of_blocks,), (number_of_threads,)](high_arr,\n",
    "                                                              low_arr,\n",
    "                                                              upD_arr,\n",
    "                                                              doD_arr,\n",
    "                                                              array_len)\n",
    "    return upD_arr, doD_arr\n",
    "\n",
    "# RSI GPU Version\n",
    "def gpu_relative_strength_index(high_arr, low_arr, n):\n",
    "    \"\"\"Calculate Relative Strength Index(RSI) for given data.\n",
    "    :param high_arr: high price of the bar, expect series from cudf\n",
    "    :param low_arr: low price of the bar, expect series from cudf\n",
    "    :param n: time steps to do EWM average\n",
    "    :return: Relative Strength Index in cudf.Series\n",
    "    \"\"\"\n",
    "    UpI, DoI = upDownMove(high_arr.to_gpu_array(), low_arr.to_gpu_array())\n",
    "    UpI_s = shift(UpI, 1)\n",
    "    UpI_s[0] = 0\n",
    "    DoI_s = shift(DoI, 1)\n",
    "    DoI_s[0] = 0\n",
    "    PosDI = Ewm(n, UpI_s).mean()\n",
    "    NegDI = Ewm(n, DoI_s).mean()\n",
    "    RSI = division(PosDI, summation(PosDI, NegDI))\n",
    "    return cudf.Series(RSI, nan_as_null=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can compare the CPU and GPU implementation, it is very similar to each other. The up and down movement part of the computation is translated into a python CUDA kernel.\n",
    "\n",
    "Here we write some visulization code backed by bqplot library to show the stock price and indicators and compare the performance of CPU RSI vs GPU RSI"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "ebc595e4f7854d299f2b6fbd43f0ad94",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Dropdown(description='Add stock', options=('stock0', 'stock1'), value=None), Int…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "add_stock_selector = widgets.Dropdown(options=list_stocks.keys(), value=None, description=\"Add stock\")\n",
    "year_selector = widgets.IntSlider(description=\"All Year\", continuous_update=False)\n",
    "year_selectors = []\n",
    "\n",
    "def get_figure(selected, df):\n",
    "    with out:\n",
    "        this_stock = one_stock(df, list_stocks[selected])\n",
    "        this_stock_store = [this_stock]\n",
    "        stock_selector = widgets.Dropdown(options=list_stocks.keys(), value=add_stock_selector.value, description=\"stock\")\n",
    "        indicator_selector = widgets.Dropdown(options=indicator_lists, value=None, description=\"Indicator\")\n",
    "        min_year = this_stock.Dte.to_array().min().astype(datetime.datetime).year\n",
    "        max_year = this_stock.Dte.to_array().max().astype(datetime.datetime).year\n",
    "        year_selector = widgets.IntSlider(min=min_year, max=max_year, description=\"Year\", continuous_update=False)\n",
    "        year = year_selector.value\n",
    "        stock = slice_stock(this_stock, year)\n",
    "        sc = LinearScale()\n",
    "        sc2 = LinearScale()\n",
    "        dt_scale = DateScale()\n",
    "        ax_x = Axis(label='Date', scale=dt_scale)\n",
    "        ax_y = Axis(label='Price', scale=sc, orientation='vertical', tick_format='0.0f')\n",
    "        # Construct the marks\n",
    "        tdp = stock.Dte.shape[0]\n",
    "        skips = tdp // plot_dp\n",
    "        ohlc = OHLC(x=stock.Dte[::skips], y=stock[['Open','High','Low', 'Close']].as_gpu_matrix()[::skips, :], marker='candle', scales={'x': dt_scale, 'y': sc}, format='ohlc',\n",
    "                    stroke='blue', display_legend=True, labels=[selected])\n",
    "        bar = Bars(x=stock.Dte[::skips], y=stock.Volume[::skips], \n",
    "               scales={'x': dt_scale, 'y': sc2}, padding=0.2)\n",
    "        def_tt = Tooltip(fields=['x', 'y'], formats=['%Y-%m-%d', '.2f'])\n",
    "        bar.tooltip = def_tt\n",
    "        bar.interactions = {\n",
    "            'legend_hover': 'highlight_axes',\n",
    "            'hover': 'tooltip', \n",
    "            'click': 'select',\n",
    "        }\n",
    "        sc.min = stock.Close.min() - 0.3 * (stock.Close.max() - stock.Close.min()) \n",
    "        sc.max = stock.Close.max()\n",
    "        sc2.max = stock.Volume.max()*4.0\n",
    "        dt_scale.min = pd.Timestamp('%d-1-1' % year)\n",
    "        dt_scale.max = pd.Timestamp('%d-1-1' % (year + 1))\n",
    "        f = Figure(axes=[ax_x, ax_y], marks=[ohlc, bar], fig_margin={\"top\":0, \"bottom\":60, \"left\":60, \"right\":60})\n",
    "        f.layout.height = main_figure_height\n",
    "        f.layout.width = figure_width\n",
    "        para_selectors = widgets.VBox([])\n",
    "        color_id = [0]\n",
    "        \n",
    "        def update_graph(stock):\n",
    "            with bar.hold_trait_notifications() as bc, ohlc.hold_trait_notifications() as oc:\n",
    "                tdp = stock.Dte.shape[0]\n",
    "                skips = tdp // plot_dp\n",
    "                ohlc.y = stock[['Open','High','Low', 'Close']].as_gpu_matrix()[::skips, :]\n",
    "                ohlc.x = stock.Dte[::skips]\n",
    "            \n",
    "                bar.y = stock.Volume[::skips]\n",
    "                bar.x = stock.Dte[::skips]\n",
    "        \n",
    "                sc.min = stock.Close.min() - 0.3 * (stock.Close.max() - stock.Close.min()) \n",
    "                sc.max = stock.Close.max()\n",
    "                sc2.max = stock.Volume.max()*4.0\n",
    "                dt_scale.min = pd.Timestamp('%d-1-1' % year_selector.value)\n",
    "                dt_scale.max = pd.Timestamp('%d-1-1' % (year_selector.value + 1))\n",
    "                update_range()\n",
    "        \n",
    "        def year_selection(*stock):\n",
    "            stock = slice_stock(this_stock_store[0], year_selector.value)\n",
    "            update_graph(stock)\n",
    "            \n",
    "        \n",
    "        def stock_selection(*stock):\n",
    "            this_stock_store[0] = one_stock(df, list_stocks[stock_selector.value])\n",
    "            year_selector.min = this_stock_store[0].Dte.to_array().min().astype(datetime.datetime).year\n",
    "            year_selector.max = this_stock_store[0].Dte.to_array().max().astype(datetime.datetime).year\n",
    "            stock = slice_stock(this_stock_store[0], year_selector.value)\n",
    "            ohlc.labels = [stock_selector.value]\n",
    "            update_graph(stock)\n",
    "            \n",
    "        def update_figure(stock, objects):\n",
    "            line = objects[0]\n",
    "            with line.hold_trait_notifications():\n",
    "                tdp = stock.Dte.shape[0]\n",
    "                skips = tdp // plot_dp\n",
    "                line.y = stock['out'][::skips]\n",
    "                line.x = stock.Dte[::skips]\n",
    "                \n",
    "        def add_new_indicator(new_fig):\n",
    "            # add new figure\n",
    "            # take the axis from the fig\n",
    "            empty = {\"top\":0, \"bottom\":0, \"left\":60, \"right\":60}\n",
    "            axis_margin = {\"top\":0, \"bottom\":60, \"left\":60, \"right\":60}\n",
    "            axes_copy = multiple_figs.children[-1].axes.copy() \n",
    "            multiple_figs.children[-1].fig_margin = empty\n",
    "            to_be_removed = axes_copy[0]\n",
    "            axes_copy.remove(to_be_removed)\n",
    "            multiple_figs.children[-1].axes = axes_copy\n",
    "            new_axes = new_fig.axes.copy()\n",
    "            new_fig.axes = [to_be_removed] + new_axes\n",
    "            new_fig.fig_margin = axis_margin\n",
    "            # add new figure\n",
    "            multiple_figs.children += (new_fig,)\n",
    "            \n",
    "        def indicator_selection(*stock):\n",
    "            if indicator_selector.value is None:\n",
    "                return\n",
    "            color_id[0] = (color_id[0] + 1) % len(CATEGORY20)\n",
    "            \n",
    "            def setup_indicator(get_para_widgets, get_parameters, process_outputs, create_figure, update_figure, indicator_fun):\n",
    "\n",
    "                with out:                    \n",
    "                    def update_df(para_selector_widgets):\n",
    "                        # with out:\n",
    "                        #    print('called')\n",
    "                        # if para_selector_widgets[-1].value==\"Busy calculating ...\":\n",
    "                        #    return\n",
    "                        my_stock = this_stock_store[0]\n",
    "                        para_selector_widgets[-1].value=\"Busy calculating ...\"\n",
    "                        parameters = get_parameters(my_stock, para_selector_widgets)\n",
    "                        beg = time.time()\n",
    "                        output = indicator_fun(*parameters)\n",
    "                        end = time.time()\n",
    "                        para_selector_widgets[-1].value=\"Computation time:%.3f on %d data points\" % (end - beg, len(parameters[0]))\n",
    "                        stock_df = process_outputs(output, my_stock)\n",
    "                        stock = slice_stock(stock_df, year_selector.value)\n",
    "                        return stock\n",
    "                    para_selector_widgets = get_para_widgets()   \n",
    "                    para_selectors.children += tuple(para_selector_widgets)                    \n",
    "                    stock = update_df(para_selector_widgets)\n",
    "                    figs = create_figure(stock)\n",
    "\n",
    "                    def update_para(*para):\n",
    "                        # with out:\n",
    "                        #    print(para)\n",
    "                        #    print('update')\n",
    "                        stock = update_df(para_selector_widgets)\n",
    "                        update_figure(stock, figs)\n",
    "                        \n",
    "                    for selector in para_selector_widgets[:-1]:\n",
    "                        selector.observe(update_para, 'value')\n",
    "                    year_selector.observe(update_para, 'value')\n",
    "                    stock_selector.observe(update_para, 'value')\n",
    "\n",
    "            if indicator_selector.value=='RSI(GPU)':\n",
    "                with out:\n",
    "                    def get_para_widgets():\n",
    "                        para_selector = widgets.IntSlider(min=2, max=60, description=\"RSI(GPU)\", continuous_update=False)\n",
    "                        label = widgets.Label(value=\"\")\n",
    "                        para_selector_widgets = [para_selector, label]                    \n",
    "                        return para_selector_widgets\n",
    "\n",
    "                    def get_parameters(stock_df, para_selector_widgets):\n",
    "                        return  (stock_df[\"High\"], stock_df[\"Low\"], para_selector_widgets[0].value)\n",
    "                    \n",
    "                    def process_outputs(output, stock_df):\n",
    "                        stock_df['out'] = output\n",
    "                        stock_df['out'] = stock_df['out'].fillna(0)\n",
    "                        return stock_df\n",
    "                                   \n",
    "                    def create_figure(stock):\n",
    "                        tdp = stock.Dte.shape[0]\n",
    "                        skips = tdp // plot_dp\n",
    "                        sc_co = LinearScale()\n",
    "                        ax_y = Axis(label='RSI(GPU)', scale=sc_co, orientation='vertical')\n",
    "                        new_line = Lines(x=stock.Dte[::skips], y=stock['out'][::skips], scales={'x': dt_scale, 'y': sc_co}, colors=[CATEGORY20[color_id[0]]])\n",
    "                        new_fig = Figure(marks=[new_line], axes=[ax_y])\n",
    "                        new_fig.layout.height = indicator_figure_height\n",
    "                        new_fig.layout.width = figure_width                    \n",
    "                        figs = [new_line]\n",
    "                        # add new figure\n",
    "                        add_new_indicator(new_fig)\n",
    "                        return figs\n",
    "\n",
    "                    \n",
    "                    indicator_fun = gpu_relative_strength_index                \n",
    "                    setup_indicator(get_para_widgets, get_parameters, process_outputs, create_figure, update_figure, indicator_fun)\n",
    "            elif indicator_selector.value=='RSI(CPU)':\n",
    "                with out:\n",
    "                    def get_para_widgets():                     \n",
    "                        para_selector = widgets.IntSlider(min=2, max=60, description=\"RSI(CPU)\", continuous_update=False)\n",
    "                        label = widgets.Label(value=\"\")\n",
    "                        para_selector_widgets = [para_selector, label]                    \n",
    "                        return para_selector_widgets\n",
    "\n",
    "                    def get_parameters(stock_df, para_selector_widgets):\n",
    "                        return  (stock_df[\"High\"], stock_df[\"Low\"], para_selector_widgets[0].value)\n",
    "                    \n",
    "                    def process_outputs(output, stock_df):\n",
    "                        stock_df['out'] = output\n",
    "                        stock_df['out'] = stock_df['out'].fillna(0)\n",
    "                        return stock_df\n",
    "                                   \n",
    "                    def create_figure(stock):\n",
    "                        tdp = stock.Dte.shape[0]\n",
    "                        skips = tdp // plot_dp\n",
    "                        sc_co = LinearScale()\n",
    "                        ax_y = Axis(label='RSI(CPU)', scale=sc_co, orientation='vertical')\n",
    "                        new_line = Lines(x=stock.Dte[::skips], y=stock['out'][::skips], scales={'x': dt_scale, 'y': sc_co}, colors=[CATEGORY20[color_id[0]]])\n",
    "                        new_fig = Figure(marks=[new_line], axes=[ax_y])\n",
    "                        new_fig.layout.height = indicator_figure_height\n",
    "                        new_fig.layout.width = figure_width                    \n",
    "                        figs = [new_line]\n",
    "                        # add new figure\n",
    "                        add_new_indicator(new_fig)\n",
    "                        return figs\n",
    "\n",
    "                    \n",
    "                    indicator_fun = cpu_rsi                \n",
    "                    setup_indicator(get_para_widgets, get_parameters, process_outputs, create_figure, update_figure, indicator_fun)\n",
    "                    \n",
    "                            \n",
    "            \n",
    "            indicator_selector.value=None\n",
    "                    \n",
    "                \n",
    "                    \n",
    "        year_selector.observe(year_selection, 'value')\n",
    "        stock_selector.observe(stock_selection, 'value')\n",
    "        indicator_selector.observe(indicator_selection, 'value')\n",
    "        multiple_figs = widgets.VBox([f])\n",
    "        return multiple_figs, year_selector, stock_selector, indicator_selector, para_selectors\n",
    "\n",
    "#f, year_selector, stock_selector, indicator_selector, para_selectors = get_figure(add_stock_selector.value, df)\n",
    "\n",
    "def stock_selection(*stock):\n",
    "    if add_stock_selector.value is None:\n",
    "        with out:\n",
    "            print('here')\n",
    "        return\n",
    "    f, year_selector, stock_selector, indicator_selector, para_selectors = get_figure(add_stock_selector.value, df)\n",
    "    vbox = w.children[1]\n",
    "    vbox.children += (widgets.HBox([f, widgets.VBox([year_selector, stock_selector, indicator_selector, para_selectors])]),) \n",
    "    with out:\n",
    "        print(widgets)\n",
    "    year_selectors.append(year_selector)\n",
    "    update_range()\n",
    "    add_stock_selector.value = None\n",
    "\n",
    "def update_range():\n",
    "    min_vals = []\n",
    "    max_vals = []\n",
    "    for i in year_selectors:\n",
    "        min_vals.append(i.min)\n",
    "        max_vals.append(i.max)\n",
    "    minV = max(min_vals)\n",
    "    maxV = min(max_vals)\n",
    "    if minV<=maxV:\n",
    "        year_selector.disabled = False\n",
    "        year_selector.max = maxV   \n",
    "        year_selector.min = minV\n",
    "    else:\n",
    "        year_selector.disabled = True\n",
    "    \n",
    "\n",
    "out = widgets.Output(layout={'border': '1px solid black'})\n",
    "\n",
    "def update_all_ranges(*arg):\n",
    "    for i in year_selectors:\n",
    "        i.value = year_selector.value\n",
    "    \n",
    "add_stock_selector.observe(stock_selection, 'value')\n",
    "year_selector.observe(update_all_ranges, 'value')\n",
    "selectors = widgets.HBox([add_stock_selector, year_selector])\n",
    "w = widgets.VBox([selectors, widgets.VBox([])])\n",
    "w"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "While playing with it, you may notice GPU RSI is very fast that it completes the computation around 10ms. The CPU RSI takes around 10s. This is about 1000x speedup. The simple example shows the data scientist just need to write a little code to translate a customized indicator into GPU computation and gains 1000x speed up. This allows massive amount of data analysis to be done in real time and data scientist can interactively play with it. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "ef7aeb5f807c4854a8b553a2b0db8dac",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output(layout=Layout(border='1px solid black'), outputs=({'name': 'stdout', 'text': \"<module 'ipywidgets' from…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "out\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
